package jssia.database.mysql.columns;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.Iterator;

/**
 * Represents a MySQL <code>SET</code> column backed by a java <code>enum</code>.
 */
public class SetColumn <T extends Enum<T>> extends EnumBackedColumn<T> {

    /**
     * Constructs a <code>Column</code> to represent a MySQL table column of type set.
     * It enforces that the members of the set column correspond to the values
     * of the <code>enum</code> of type <code>clas</code>.
     *
     * @param clas the class of the <code>enum</code> that defines all valid members
     * of the MySQL set column.
     */
    public SetColumn(Class<T> clas) {
        super(clas);
    }

    /**
     *
     * @param rs the <code>ResultSet</code> that contains a row result for this column.
     * @return an <code>EnumSet</code> of the values for this column within the <code>ResultSet rs</code>.
     * @throws SQLException
     */
    public EnumSet<T> get(ResultSet rs) throws SQLException {
        final EnumSet<T> set = EnumSet.noneOf(enumType);
        final String result = rs.getString(fullyQualifiedName);
        if (result != null && result.length() > 0) {
            for (String setMember : result.split(",")) {
                set.add(Enum.valueOf(enumType, setMember.toUpperCase()));
            }
        }
        return set;
    }

    /**
     *
     * @param rs the <code>ResultSet</code> that contains a row result for this column.
     * @return a comma delimited <code>String</code> of the values for this column within the <code>ResultSet rs</code>.
     * @throws SQLException
     */
    public String getString(ResultSet rs) throws SQLException {
        return rs.getString(fullyQualifiedName);
    }

    /**
     * Sets the value <code>set</code> to the <code>PreparedStatement ps</code> at the given index.
     * If a string exists in <code>set</code> that does not correspond to an <code>enum</code>
     * backing this column an IllegalArgumentException will be thrown.
     *
     * @param ps the <code>PreparedStatement</code> used to set the parameter <code>set</code> to.
     * @param index the parameter index for the parameter <code>set</code>.
     * @param set the parameter value.
     * @throws SQLException
     */
    public void set(PreparedStatement ps, int index, String[] set) throws SQLException {
        set(ps, index, Arrays.asList(set));
    }

    /**
     * Sets the value <code>set</code> to the <code>PreparedStatement ps</code> at the given index.
     * If a string exists in <code>set</code> that does not correspond to an <code>enum</code>
     * backing this column an IllegalArgumentException will be thrown.
     *
     * @param ps the <code>PreparedStatement</code> used to set the parameter <code>set</code> to.
     * @param index the parameter index for the parameter <code>set</code>.
     * @param set the parameter value.
     * @throws SQLException
     */
    public void set(PreparedStatement ps, int index, Collection<String> set) throws SQLException {
        ps.setString(index, convertListToStringOfEnumStrings(set, enumType, false));
    }

    /**
     * Sets the value <code>set</code> to the <code>PreparedStatement ps</code> at the given index.
     * If a string exists in <code>set</code> that does not correspond to one of the valid set
     * members for this column it will be silently ignored.
     *
     * @param ps the <code>PreparedStatement</code> used to set the parameter <code>set</code> to.
     * @param index the parameter index for the parameter <code>set</code>.
     * @param set the parameter value.
     * @throws SQLException
     */
    public void setIgnore(PreparedStatement ps, int index, String[] set) throws SQLException {
        setIgnore(ps, index, Arrays.asList(set));
    }

    /**
     * Sets the value <code>set</code> to the <code>PreparedStatement ps</code> at the given index.
     * If a string exists in <code>set</code> that does not correspond to one of the valid set
     * members for this column it will be silently ignored.
     *
     * @param ps the <code>PreparedStatement</code> used to set the parameter <code>set</code> to.
     * @param index the parameter index for the parameter <code>set</code>.
     * @param set the parameter value.
     * @throws SQLException
     */
    public void setIgnore(PreparedStatement ps, int index, Collection<String> set) throws SQLException {
        ps.setString(index, convertListToStringOfEnumStrings(set, enumType, true));
    }

    /**
     * Sets the value <code>set</code> to the <code>PreparedStatement ps</code> at the given index.
     *
     * @param ps the <code>PreparedStatement</code> used to set the parameter <code>set</code> to.
     * @param index the parameter index for the parameter <code>set</code>.
     * @param set the parameter value.
     * @throws SQLException
     */
    public void set(PreparedStatement ps, int index, EnumSet<T> set) throws SQLException {
        ps.setString(index, convertEnumSetToString(set));
    }

    /**
     * The purpose of this method is to convert a collection of strings to a single comma delimited <code>String</code> to provide
     * easy insertions and updates to existing MySQL set columns.
     *
     * @param list the <code>Iterator</code> referring to a collection of strings to be converted to a comma delimited <code>String</code>.
     * @param clas the <code>enum</code> that each <code>String</code> within <code>list</code> must correspond to.
     * @param ignore if this is true then an IllegalArgumentException</code> will not be thrown for strings that do not match a
     *           corresponding <code>enum</code> of <code>clas</code>.  It will silently ignore all strings that do not correspond to members of <code>clas</code>.
     *
     * @return a <code>String</code> of comma delimited strings corresponding to the <code>String</code> values in <code>list</code>.
     */
    private static <E extends Enum<E>> String convertListToStringOfEnumStrings(Iterable<String> list, Class<E> clas, boolean ignore) {
        Iterator<String> i = list.iterator();
        if (! i.hasNext())
            return "";

        StringBuilder sb = new StringBuilder();
        for (;;) {
            String e = i.next();
            try {
                sb.append(Enum.valueOf(clas, e.toUpperCase()));
            } catch(IllegalArgumentException iae) {
                if (!ignore)
                    throw iae;
            }
            if (! i.hasNext())
                return sb.toString();
            sb.append(",");
        }
    }

    /**
     * The purpose of this method is to convert an <code>EnumSet</code> to a single comma delimited <code>String</code> to provide
     * easy insertions and updates to existing MySQL set columns.
     *
     * @param set the <code>EnumSet</code> to be converted to a comma delimited <code>String</code>.
     * @return a <code>String</code> of comma delimited strings corresponding to the enumeration values in <code>set</code>.
     */
    private static <E extends Enum<E>> String convertEnumSetToString(EnumSet<E> set) {
        Iterator<E> i = set.iterator();
        if (! i.hasNext())
            return "";

        StringBuilder sb = new StringBuilder();
        for (;;) {
            E e = i.next();
            sb.append(e);
            if (! i.hasNext())
                return sb.toString();
            sb.append(",");
        }
    }
}
